#include "esp_camera.h"
#include "WiFi.h"
#include "ArduinoWebsockets.h"
#include "Preferences.h"
#include "WebServer.h"
#include "ArduinoJson.h"

using namespace websockets;

Preferences prefs;
// WebSocket 服务器地址
String WS_URL = "ws://192.168.3.181:8282";
WebsocketsClient client;
WebServer server(80);
// 推流状态
struct ServiceConfig {
  bool streaming = false;
  int fps = 2;
  unsigned long lastFrame = 0;
  unsigned long lastPing = 0;
} config;

// WebSocket 重连控制
struct SocketState {
  int reconnectAttempts = 0;
  int maxReconnectAttempts = 30;
  unsigned long reconnectDelay = 1000;  // 初始 1 秒
  unsigned long nextReconnectTime = 0;
} socketState;

// 摄像头配置
#define PWDN_GPIO_NUM 32   // 摄像头掉电控制；32 接低电平供电，-1 表示未使用
#define RESET_GPIO_NUM -1  // 摄像头硬复位引脚；-1 表示未连接
#define XCLK_GPIO_NUM 0    // 摄像头外部时钟 XCLK
#define SIOD_GPIO_NUM 26   // SCCB（I2C 类似）数据线
#define SIOC_GPIO_NUM 27   // SCCB 时钟线
#define Y9_GPIO_NUM 35     // 摄像头数据位 D7
#define Y8_GPIO_NUM 34     // 摄像头数据位 D6
#define Y7_GPIO_NUM 39     // 摄像头数据位 D5
#define Y6_GPIO_NUM 36     // 摄像头数据位 D4
#define Y5_GPIO_NUM 21     // 摄像头数据位 D3
#define Y4_GPIO_NUM 19     // 摄像头数据位 D2
#define Y3_GPIO_NUM 18     // 摄像头数据位 D1
#define Y2_GPIO_NUM 5      // 摄像头数据位 D0
#define VSYNC_GPIO_NUM 25  // 同步信号 VSYNC
#define HREF_GPIO_NUM 23   // 行参考 HREF
#define PCLK_GPIO_NUM 22   // 像素时钟 PCLK

// ==== 初始化摄像头 ====
/**
 * @brief 初始化摄像头配置
 * @return bool 返回初始化是否成功，成功返回true，失败返回false
 */
bool setupCamera() {
  // 定义摄像头配置结构体
  camera_config_t config;
  // 配置LED PWM通道和定时器
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  // 配置数据引脚D0-D7
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;

  // 配置控制引脚
  config.pin_xclk = XCLK_GPIO_NUM;    // 时钟引脚
  config.pin_pclk = PCLK_GPIO_NUM;    // 像素时钟引脚
  config.pin_vsync = VSYNC_GPIO_NUM;  // 垂直同步引脚
  config.pin_href = HREF_GPIO_NUM;    // 水平参考引脚

  // 配置I2C引脚
  config.pin_sscb_sda = SIOD_GPIO_NUM;  // I2C数据引脚
  config.pin_sscb_scl = SIOC_GPIO_NUM;  // I2C时钟引脚

  // 配置电源和复位引脚
  config.pin_pwdn = PWDN_GPIO_NUM;    // 电源使能引脚
  config.pin_reset = RESET_GPIO_NUM;  // 复位引脚

  // 配置时钟频率和像素格式
  config.xclk_freq_hz = 20000000;  // JPEG格式输出
  config.pixel_format = PIXFORMAT_JPEG;

  // 根据是否检测到PSRAM来配置不同的参数
  if (psramFound()) {
    config.frame_size = FRAMESIZE_VGA;
    config.jpeg_quality = 12;
    config.fb_count = 2;
  } else {
    config.frame_size = FRAMESIZE_QVGA;
    config.jpeg_quality = 15;
    config.fb_count = 1;
  }

  // 初始化摄像头并检查是否成功
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("[CAM] init failed: 0x%x\n", err);
    return false;
  }

  // 获取摄像头传感器并设置镜像和翻转
  sensor_t* s = esp_camera_sensor_get();
  s->set_vflip(s, 1);    // 设置垂直翻转
  s->set_hmirror(s, 1);  // 设置水平镜像
  return true;
}
// ==== 初始化 Wi-Fi STA ====
/**
 * 连接WiFi网络的函数（站点模式）
 * @return 连接成功返回true，失败返回false
 */
bool connectWiFiSTA() {
  // 打开存储空间，名称为"wifi"，参数true表示只读模式
  prefs.begin("wifi", true);
  // 从存储中获取WiFi SSID，如果不存在则使用默认值DEFAULT_SSID
  String wifi_ssid = prefs.getString("ssid");
  // 从存储中获取WiFi密码，如果不存在则使用默认值DEFAULT_PASS
  String wifi_pass = prefs.getString("pass");
  // 关闭存储空间
  prefs.end();
  // 设置WiFi模式为STA（站点模式）
  WiFi.mode(WIFI_STA);
  // 打印正在连接的WiFi名称
  Serial.print("Connecting to WiFi: ");
  Serial.println(wifi_ssid);
  WiFi.setSleep(false);  // 禁用WiFi睡眠模式以提高稳定性
  // 开始连接WiFi网络
  WiFi.begin(wifi_ssid.c_str(), wifi_pass.c_str());
  // 记录开始连接的时间
  unsigned long start = millis();
  // 等待连接成功或超时（10秒）
  // WiFi.status() != WL_CONNECTED 表示未连接
  // millis() - start < 10000 表示未超时10秒
  while (WiFi.status() != WL_CONNECTED && millis() - start < 10000) {
    delay(500);         // 每500毫秒检查一次
    Serial.print(".");  // 打印连接进度点
  }
  Serial.print();
  // 检查连接状态
  if (WiFi.status() == WL_CONNECTED) {
    // 连接成功，打印IP地址
    Serial.print("WiFi connected, IP: ");
    return true;  // 返回连接成功
  } else {
    // 连接失败，打印失败信息
    Serial.print("WiFi connection failed!");
    return false;  // 返回连接失败
  }
}
// ==== Wi-Fi 配置保存 ====
/**
 * 保存WiFi凭据到非易失性存储器中
 * @param WiFi网络的名称(SSID)
 * @param WiFi网络的密码
 */
void saveWiFi(const char* ssid, const char* pass) {
  prefs.begin("wifi", false);     // 打开名为"wifi"的命名空间，false表示可读写
  prefs.putString("ssid", ssid);  // 将SSID保存到命名空间中
  prefs.putString("pass", pass);  // 将密码保存到命名空间中
  prefs.end();                    // 关闭preferences，完成写入操作
}

// ==== Web 配置页面 ====
/**
 * 处理根路径请求的函数
 * 当用户访问设备的根URL时，此函数会被调用
 * 它会生成一个包含WiFi配置表单的HTML页面并返回给客户端
 */
void handleRoot() {
  // 创建一个包含WiFi设置表单的HTML字符串
  String html = "<html><body><h2>ESP32-CAM WiFi Setup</h2>"
                "<form action='/wifi' method='POST'>"
                "SSID:<input type='text' name='ssid'><br>"
                "Password:<input type='password' name='pass'><br>"
                "<input type='submit' value='Save'></form></body></html>";
  // 向客户端发送HTTP响应
  // 参数：状态码200(表示成功)、内容类型(text/html)、HTML内容
  server.send(200, "text/html", html);
}

/**
 * 处理WiFi配置请求的函数
 * 当接收到包含WiFi名称和密码的请求时，保存配置并重启设备
 * 如果缺少必要参数，返回错误信息
 */
void handleWiFi() {
  // 检查请求中是否包含SSID和密码参数
  if (server.hasArg("ssid") && server.hasArg("pass")) {
    // 获取WiFi名称参数
    const char* ssid = server.arg("ssid").c_str();
    // 获取WiFi密码参数
    const char* pass = server.arg("pass").c_str();
    // 保存WiFi配置信息
    saveWiFi(ssid, pass);
    // 发送成功响应并提示设备将重启
    server.send(200, "text/html", "Saved! Rebooting...");
    // 延迟2秒等待客户端接收响应
    delay(2000);
    // 重启设备以应用新的WiFi配置
    ESP.restart();
  } else {
    // 如果缺少必要参数，发送错误响应
    server.send(400, "text/plain", "Missing SSID or password");
  }
}

// ==== 启动 AP 热点 ====
/**
 * 启动ESP32的AP（接入点）模式
 * 该函数将ESP32设置为WiFi热点，使其他设备可以连接到它
 * 同时设置Web服务器，用于处理WiFi配置请求
 */
void startAPMode() {
  Serial.print("Starting AP mode...");  // 通过串口打印启动AP模式的信息
  WiFi.mode(WIFI_AP);                     // 设置WiFi模式为AP模式（接入点模式）
  // 创建名为"ESP32-CAM-Setup"的WiFi热点，密码为"12345678"
  WiFi.softAP("ESP32-CAM-Setup", "12345678");
  // 打印AP的IP地址到串口，方便用户连接后访问
  Serial.print("AP IP: ");
  Serial.println(WiFi.softAPIP());
  // 设置Web服务器的路由处理函数
  server.on("/", handleRoot);                 // 根路径"/"的处理函数
  server.on("/wifi", HTTP_POST, handleWiFi);  // "/wifi"路径的POST请求处理函数
  server.begin();
}

// ==== WebSocket 事件 ====
/**
 * 处理接收到的WebSocket消息回调函数
 * @param message 接收到的WebSocket消息对象
 */
void onMessageCallback(WebsocketsMessage message) {
  // 将消息数据转换为String类型并打印
  const char* data = message.c_str();
  Serial.print("WS RX: ");
  Serial.println(data);
  // 创建一个256字节的静态JSON文档
  StaticJsonDocument<64> doc;
  // 尝试解析JSON数据
  DeserializationError error = deserializeJson(doc, data);
  // 如果解析失败，打印错误信息并返回
  if (error) {
    Serial.print("Invalid JSON");
    return;
  }
  // 修改处理逻辑
  const char* type = doc["type"];  // 使用const char*替代String
  if (strcmp(type, "start") == 0) {
    config.streaming = true;
  } else if (strcmp(type, "stop") == 0) {
    config.streaming = false;
  } else if (strcmp(type, "fps") == 0) {
    config.fps = doc["data"].as<int>();
  }
}
/**
 * Websockets事件回调函数，用于处理各种WebSocket事件
 * @param event 发生的WebSocket事件类型
 * @param data 事件相关数据
 */
void onEventsCallback(WebsocketsEvent event, String data) {
  // 处理连接打开事件
  if (event == WebsocketsEvent::ConnectionOpened) {
    Serial.print("Connnection Opened");
    socketState.reconnectAttempts = 0;  // 成功后重置计数
    socketState.reconnectDelay = 1000;  // 重置延迟
  } else if (event == WebsocketsEvent::ConnectionClosed) {
    Serial.print("Connnection Closed");
  } else if (event == WebsocketsEvent::GotPing) {
    Serial.print("Got a Ping!");
  } else if (event == WebsocketsEvent::GotPong) {
    Serial.print("Got a Pong!");
  }
}

/**
 * 连接WebSocket函数
 * 该函数用于建立WebSocket连接，并设置重连机制
 */
void connectWebSocket() {
  // 设置消息回调函数，当收到WebSocket消息时将调用onMessageCallback
  client.onMessage(onMessageCallback);
  // 设置事件回调函数，当WebSocket事件发生时将调用onEventsCallback
  client.onEvent(onEventsCallback);
  // 检查重连次数是否小于最大重连尝试次数
  if (socketState.reconnectAttempts < socketState.maxReconnectAttempts) {
    // 增加重连尝试次数
    socketState.reconnectAttempts++;
    // 计算下次重连时间，为当前时间加上重连延迟
    socketState.nextReconnectTime = millis() + socketState.reconnectDelay;
    // 使用指数退避算法计算重连延迟，最大不超过10秒
    socketState.reconnectDelay = min(socketState.reconnectDelay * 2, (unsigned long)10000);  // 最大 10 秒
    // 通过串口打印重连信息，包括当前重连尝试次数和延迟时间
    Serial.printf("Reconnect attempt %d scheduled in %lu ms\n", socketState.reconnectAttempts, socketState.reconnectDelay);
  }
  // 执行WebSocket连接，连接到指定的URL
  client.connect(WS_URL);
}

// ==== 上报状态 ====
/**
 * 发送设备状态信息到客户端
 * 该函数创建一个JSON文档，包含WiFi信息、流状态、帧率和重连尝试次数
 * 然后将JSON序列化并通过WebSocket发送给客户端
 */
void sendStatus() {
  // 创建一个容量为256字节的静态JSON文档
  StaticJsonDocument<64> doc;
  // 设置消息类型为"status"
  doc["type"] = "ping";
  // 创建一个名为"data"的嵌套JSON数组
  JsonArray data = doc.createNestedArray("data");
  // 向数组中添加WiFi信号强度（RSSI）
  data.add(WiFi.RSSI());
  // 向数组中添加流媒体状态（布尔值）
  data.add(config.streaming);
  // 向数组中添加当前帧率（FPS）
  data.add(config.fps);
  // 创建一个空字符串用于存储序列化后的JSON
  String json;
  // 将JSON文档序列化为字符串
  serializeJson(doc, json);
  // 通过WebSocket客户端发送JSON数据
  client.send(json);
}

/**
 * 打印ESP32开发板的内存使用情况信息
 * 包括堆内存、PSRAM（如果可用）以及各任务的堆栈使用情况
 */
void printMemoryInfo() {
    // 打印内存信息标题
    Serial.println("\n=== Memory Info ===");
    // 打印堆内存信息
    Serial.printf("Total heap: %d bytes\n", ESP.getHeapSize());      // 总堆内存大小
    Serial.printf("Free heap: %d bytes\n", ESP.getFreeHeap());      // 当前可用堆内存
    Serial.printf("Min free heap: %d bytes\n", ESP.getMinFreeHeap()); // 历史最小可用堆内存
    Serial.printf("Max alloc heap: %d bytes\n", ESP.getMaxAllocHeap()); // 最大可分配堆内存
    
    // 检查并打印PSRAM信息（如果可用）
    if (psramFound()) {
        Serial.printf("Total PSRAM: %d bytes\n", ESP.getPsramSize());  // 总PSRAM大小
        Serial.printf("Free PSRAM: %d bytes\n", ESP.getFreePsram());  // 当前可用PSRAM
    }
    
    // 添加任务堆栈信息
    Serial.printf("Arduino loop task stack: %d bytes\n", uxTaskGetStackHighWaterMark(NULL));
    
    // 添加摄像头任务堆栈信息（如果存在）
    TaskHandle_t camTask = xTaskGetHandle("cam");
    if (camTask != NULL) {
        Serial.printf("Camera task stack: %d bytes\n", uxTaskGetStackHighWaterMark(camTask));
    }
    
    Serial.println("==================\n");
}


/**
 * 从摄像头获取图像数据并通过客户端流式传输
 * 该函数会检查流式传输状态、客户端可用性以及帧率限制
 * 然后获取图像帧、处理并发送数据
 */
void pumpStream() {
  // 检查是否处于流式传输状态且客户端可用
  if (!config.streaming || !client.available()) return;
  // 控制帧率，确保按照设定的fps发送图像
  if (millis() - config.lastFrame < (1000 / config.fps)) return;
  
  // 更新上一帧的时间戳
  config.lastFrame = millis();
  camera_fb_t* fb = esp_camera_fb_get();  // 获取相机帧缓冲区
  if (!fb) {
    Serial.print("Camera capture failed");
    return;
  }
  //printMemoryInfo();
  // 分配临时缓冲区来存储图像数据
  // uint8_t* tempBuffer = (uint8_t*)malloc(fb->len);
  // if (tempBuffer) {
  //   memcpy(tempBuffer, fb->buf, fb->len);
  //   esp_camera_fb_return(fb);                             // 先释放帧缓冲区
  //   client.sendBinary((const char*)tempBuffer, fb->len);  // 发送二进制图像数据
  //   free(tempBuffer);                                     // 释放临时缓冲区
  // } else {
  //   Serial.print("Failed to allocate memory for image buffer");
  //   esp_camera_fb_return(fb);  // 释放帧缓冲区
  // }
  //Serial.print("send image \n");
  client.sendBinary((const char*)fb->buf, fb->len);
  esp_camera_fb_return(fb);  // 释放帧缓冲区
}

// ==== setup ====
/**
 * 初始化函数，用于设置Arduino环境和启动相机
 * 该函数在程序开始时执行一次，负责初始化串口、相机、WiFi连接等
 */
void setup() {
  // 初始化串口通信，波特率设置为115200
  Serial.begin(115200);
  // 关闭调试输出，以减少串口输出量
  Serial.setDebugOutput(false);
  // 尝试初始化相机，如果失败则输出错误信息并进入死循环
  if (!setupCamera()) {
    // 如果初始化失败，打印错误信息并进入死循环
    Serial.print("[FATAL] Camera init failed");
    while (1) delay(1000);
  }
  // 尝试连接WiFi Station模式
  if (!connectWiFiSTA()) {
    // 如果连接失败，启动AP模式作为备选方案
    startAPMode();
  } else {
    // 如果连接成功，建立WebSocket连接
    connectWebSocket();
  }
}

// ==== loop ====
/**
 * 主循环函数，处理设备的主要逻辑
 * 包括WiFi模式处理、WebSocket连接维护、心跳检测和图像数据流传输
 */
void loop() {
  // 如果当前模式为AP模式（接入点模式），则处理HTTP客户端请求并返回
  if (WiFi.getMode() == WIFI_AP) {
    server.handleClient();
    return;
  }

  // 检查WebSocket客户端是否有数据可用
  // 如果没有数据且未超过最大重连次数，且到达重连时间，则尝试重新连接WebSocket
  if (client.available()) {
    client.poll();
  } else if (socketState.reconnectAttempts < socketState.maxReconnectAttempts && millis() > socketState.nextReconnectTime) {
    connectWebSocket();
  }

  // 心跳 + 状态上报
  // 每10秒发送一次心跳包和设备状态，保持连接活跃
  if (client.available() && millis() - config.lastPing > 10000) {
    sendStatus();
    config.lastPing = millis();
  }

  // 图像数据流处理
  // 当处于流传输模式且客户端可用时，按照设定的帧率发送图像数据
  pumpStream();
}